<?php

class ClassStructure_Weaved extends ClassStructure_Abstract {

    const WEAVE_ADVICE_BEFORE_BASE = 'weave.before';
    const WEAVE_ADVICE_AFTER_BASE = 'weave.after';
    const WEAVE_ADVICE_ON_EXCEPTION = 'weave.exception';
    
    const OPTION_BREAK_CHAIN_ON_RETURN = 'break_chain_on_return';
    
    /**
     * A reference to the internal object, which will be the base element; use the 
     * corresponding method to retrieve this internal object.
     *
     * @access private
     * @var object $obj_internal A reference to the internal object.
     */
    private $obj_internal = null;
    
    /**
     * An array of callable code advices, which may be invoked before or after
     * a particular method signature.
     *
     * @access private
     * @var array $arr_advices An array of callable code advices.
     */
    private $arr_advices = array();
    
    /**
     * An array of available methods, and their signatures, as defined from the 
     * internal object.
     *
     * @access private
     * @var array $arr_methods An array of available methods.
     */
    private $arr_methods = array();
    
    /**
     * Class constructer: encapsulates the supplied object and prepares this class
     * with information about the available method signatures.
     *
     * @access public
     * @param object $obj The object to encapsulate and become a base element.
     * @return object
     */
    public function __construct($obj) {
        
        parent::__construct();
        
        // store an internal reference of the base layer object
        $this->obj_internal = $obj;
        
        // setup the before and after layers structure
        $this->__unweave();
        
        // parse available methods from the base layer object
        $this->arr_methods = $this->parseAvailableMethods($obj);
        
    }
    
    /***************************************************************************
    / Object reflection and structural methods
    /**************************************************************************/
    
    /**
     * Gets the base object.
     *
     * @access public
     * @return object The base object.
     */
    public function __getObject() {
        
        return $this->obj_internal;
        
    }
    
    /**
     * Gets the class name of the base object.
     *
     * If the base object is an instance of this class, then get
     * the class name by invoking this same method on the base object.
     *
     * This method uses the PHP function get_class().
     * 
     * @access public 
     * @return string The class name of the base object.
     */
    public function __getClass() {
        
        // special case: recursively get the class name of the base object
        // if the current internal reference is an instance of this class.
        if($this->obj_internal instanceof self) {
            return $this->obj_internal->__getClass($str_class);
        }
        
        // normal case: use PHP's function to return the class name
        return get_class($this->obj_internal);
        
    }
    
    /**
     * Gets the parent class name of the base layer object.
     *
     * If the base layer object is an instance of this class, then get
     * the parent name by invoking this same method on the base layer object.
     *
     * This method uses the PHP function get_parent_class().
     * 
     * @access public 
     * @return string The parent class name of the base layer object.
     */
    public function __getParentClass() {
        
        // special case: recursively get the parent name of the base layer object
        // if the current internal reference is an instance of this class.
        if($this->obj_internal instanceof ClassStructure_Abstract) {
            return $this->obj_internal->__getParentClass($str_class);
        }
        
        // normal case: use PHP's function to return the parent name
        return get_parent_class($this->obj_internal);
        
    }
    
    /**
     * Determines whether the parameter is an instance of the base object.
     *
     * If the base object is an instance of this class, then resolve this
     * by invoking this same method on the base object.
     *
     * This method uses the PHP operator instanceof.
     * 
     * @access public
     * @param string|object $mix_class The class name or object to compare.
     * @return boolean Whether the parameter is an instance of the base object.
     */
    public function __isInstanceOf($mix_class) {
        
        // standardise parameter into a class name
        $str_class = is_object($mix_class) 
            ? get_class($mix_class) 
            : (string) $mix_class;
        
        // internal case: use PHP's operator to return the instance comparison.
        $bln_internal = ($this->obj_internal instanceof $str_class);
        
        // normal case: also check the instance of this
        $bln_this = ($this instanceof $str_class);
        
        return ($bln_internal || $bln_this);
        
    }
    
    /**
     * Determines whether the parameter is a subclass of the base object.
     *
     * If the base object is an instance of this class, then resolve this
     * by invoking this same method on the base object.
     *
     * This method uses the PHP function is_subclass_of.
     * 
     * @access public
     * @param string|object $mix_class The class name or object to compare.
     * @return boolean Whether the parameter is an instance of the base object.
     */
    public function __isSubclassOf($mix_class) {
        
        // standardise parameter into a class name
        $str_class = is_object($mix_class) 
            ? get_class($mix_class) 
            : (string) $mix_class;
        
        // internal case: use PHP's operator to return the subclass comparison.
        $bln_internal = is_subclass_of($this->obj_internal, $str_class);
        
        // normal case: also check the subclass of this
        $bln_this = is_subclass_of($this, $str_class);
        
        return ($bln_internal || $bln_this);
        
    }
    
    /**
     * Determines whether the named method exists in the base object.
     *
     * @access public
     * @param string $str_method The method to check for existence.
     * @return boolean
     */
    public function __hasMethod($str_method) {
        
        return array_key_exists($str_method, $this->arr_methods);
        
    }
    
    /**
     * Gets the class methods of the object and its extensions.
     *
     * This method uses the PHP function get_class_methods().
     * 
     * @access public 
     * @param string $str_virtual_prefix The prefix to give methods that are not
     *      actually defined at compile time.
     * @return string The class name of the base object.
     */
    public function __getClassMethods($str_virtual_prefix = '') {
        
        $arr_class_methods = array();
        
        $arr_methods = get_class_methods($this);
        foreach($arr_methods as $str_method) {
            $arr_class_methods[$str_method] = $str_method;
        }
        
        
        $arr_methods = get_class_methods($this->obj_internal);
                
        foreach($arr_methods as $str_method) {
            if(strpos($str_method, '__') !== 0) {
                $str_method = $str_virtual_prefix . $str_method;
                $arr_class_methods[$str_method] = $str_method;
            }
        }
        
        return array_values($arr_class_methods);
        
    }
    
    /**
     * Gets the class method signatures of the object and base object.
     *
     * This method uses the PHP function get_class_methods().
     * 
     * @access public 
     * @param string $str_virtual_prefix The prefix to give methods that are not
     *      actually defined at compile time.
     * @return string The class name of the base object.
     */
    public function __getMethodSignatures($str_virtual_prefix = '@') {
        
        $arr_signatures = array();
        
        $arr_methods = self::getMethodSignatures($this->obj_internal);
        foreach($arr_methods as $str_method => $arr_method) {
            $arr_method['visibility'] = 'virtual';
            $arr_signatures[$str_method] = self::formatMethodSignature($str_method, $arr_method);
        }
        
        $arr_methods = self::getMethodSignatures($this);
        foreach($arr_methods as $str_method => $arr_method) {
            $arr_signatures[$str_method] = self::formatMethodSignature($str_method, $arr_method);
        }
        
        return $arr_signatures;
        
    }

    
    /***************************************************************************
    / Method invocation
    /**************************************************************************/
    
    /**
     * Delegates the invoked method with the defined advices.
     *
     * Only the return value of the base object's method will be returned, unless
     * specifically determined by the advice.
     *
     * @final final
     * @access protected
     * @param string $str_method The method name to invoke, intended for the internal object.
     * @param array $arr_parameters The method's parameters, gathered in array format.
     * @return mixed
     */
    final protected function delegateMethod($str_method, $arr_parameters) {
        
        $mix_return = null;
        
        // does this method exist for the internal object?
        $this->ensureMethodExists($str_method);
        
        //----------------------------------------------------------------------
        // prepare for method invocation
        
        // create an array of parameter references - this will allow each advice
        // to modify the parameters before and after the base method invocation
        
        $arr_parameters_by_reference = array();
        foreach($arr_parameters as $mix_key => $mix_value) {
            $arr_parameters_by_reference[$mix_key] = &$mix_value;
        }
        
        //----------------------------------------------------------------------
        // core algorithm to delegate appropriate advice and base method
        
        try {
            
            // delegate before-base advice
            $mix_return_before = $this->delegateAdvice(self::WEAVE_ADVICE_BEFORE_BASE, $str_method, $arr_parameters_by_reference);
            if(!is_null($mix_return_before)) {
                return $mix_return_before;
            }
            
            // generate base method callback
            $clb_internal = array($this->obj_internal, $str_method);
            
            // store the return value of the base method
            $mix_return = call_user_func_array($clb_internal, $arr_parameters_by_reference);
            
            // delegate after-base advice
            $mix_return_after = $this->delegateAdvice(self::WEAVE_ADVICE_AFTER_BASE, $str_method, $arr_parameters_by_reference);
            if(!is_null($mix_return_after)) {
                return $mix_return_after;
            }
            
        } catch (Exception $obj_exception) {
            
            if($this->__hasAdvice($str_method, self::WEAVE_ADVICE_ON_EXCEPTION)) {
                        
                // delegate on-exception advice, using only the exception as the 
                // advice parameters
                
                $arr_parameters = array($obj_exception);
                $this->delegateAdvice(self::WEAVE_ADVICE_ON_EXCEPTION, $str_method, $arr_parameters);
                
                // the on-exception advice may throw exceptions itself, but let the
                // client script handle this
            
            } 
            
            // if there are no on-exception advices, then throw the exception to
            // the client script
            else {
            
                throw $obj_exception;
            
            }
            
        }
        
        // return the appropriate return value
        return $mix_return;
        
    }
    
    final protected function delegateAdvice($enm_advice, $str_method, $arr_parameters) {
        
        $mix_return = null;
        
        // are there layers which should be called before the base layer?
        if(array_key_exists($str_method, $this->arr_advices[$enm_advice])) {
            
            $arr_advices = $this->arr_advices[$enm_advice][$str_method];
            
            // iterate through each layer and execute the corresponding callback
            foreach($arr_advices as $i => $arr_advice) {
                
                $clb_callable = $arr_advice['callable'];
                
                // execute the advice
                $mix = call_user_func_array($clb_callable, $arr_parameters);
                
                if(
                    in_array($enm_advice, array(
                        self::WEAVE_ADVICE_BEFORE_BASE,
                        self::WEAVE_ADVICE_AFTER_BASE
                    )) &&
                    !is_null($mix) && 
                    isset($arr_advice[self::OPTION_BREAK_CHAIN_ON_RETURN]) &&
                    ($arr_advice[self::OPTION_BREAK_CHAIN_ON_RETURN] === true)
                ) {
                    return $mix;
                }
            }
            
        }
        
        return $mix_return;
    }
    
    /**
     * Adds a callable function/method into the invocation chain before or after
     * the base method.
     *
     * The parameter signature of the callable element must match that of the
     * base layer method - or have no parameters at all.
     *
     * This method uses PHP's Reflection classes.
     *
     * @final final 
     * @access protected
     * @param callback $clb_callable The callable function/method, as defined by
     *      the is_callable() function.
     * @param string $str_method The base method to attach this callback layer.
     * @param string $enm_order Whether to put the method invocation before or
     *      after the base method. Defaults to 'after'.
     * @param array $arr_options An array of layer options.
     * @return void
     */
    final protected function __weaveAdvice($clb_callable, $mix_pointcut, $enm_type, $arr_options = array()) {
        
        //----------------------------------------------------------------------
        // validate parameters as necessary
        
        // validate the advice parameter
        if(!is_callable($clb_callable)) {
            throw new ClassStructure_Exception_CallbackInvalid($clb_callable);
        }
        
        //----------------------------------------------------------------------
        // standardise the pointcut into an array of methods.
        
        $arr_methods = array();
        
        // case: single method reference, provided as a straightforward, direct
        // match using a string.
        if(is_string($mix_pointcut)) {
            $arr_methods[] = $mix_pointcut;
        }
        
        // case: a pointcut object, which may match multiple methods - thus,
        // determine which methods are applicable.
        else if(
            is_object($mix_pointcut) && 
            ($mix_pointcut instanceof ClassStructure_PointCut)
        ) {
        
            foreach($this->arr_methods as $str_method => $obj_method) {
                if($mix_pointcut->matchesMethod($str_method)) {
                    $arr_methods[] = $str_method;
                }
            }
        
        }
        
        //----------------------------------------------------------------------
        // standardise advice into Reflection classes
        
        $obj_callable = null;
        
        // case: the callback element is a function
        if(is_string($clb_callable)) {
            
            $obj_callable = new ReflectionFunction($clb_callable);
            
        } 
        
        // case: the callback element is an object method
        else if(is_array($clb_callable)) {
            
            $mix_callable_object = $clb_callable[0];
            $str_callable_method = $clb_callable[1];
            
            $str_callable_class = get_class($mix_callable_object);
            
            $obj_callable = new ReflectionMethod($str_callable_class, $str_callable_method);
            
        }
        
        //----------------------------------------------------------------------
        // weave advice to each matching pointcut
        
        foreach($arr_methods as $str_method) {
            
            // does this method exist for the internal object?
            $this->ensureMethodExists($str_method);
            
            // check the the parameter signature matches that of the base layer 
            // method, or ensure it has no parameters
            
            if(
                (in_array($enm_type, array(self::WEAVE_ADVICE_BEFORE_BASE, self::WEAVE_ADVICE_AFTER_BASE)) &&
                (
                    $this->matchesParameterSignature($str_method, $obj_callable) ||
                    !$this->hasParameters($obj_callable)
                )) ||
                (($enm_type == self::WEAVE_ADVICE_ON_EXCEPTION) &&
                (
                    ($this->countParameters($obj_callable) == 1) ||
                    !$this->hasParameters($obj_callable)
                ))
            ) {
                
                $this->addAdvice($str_method, $enm_type, $clb_callable, $arr_options);
                
            } else {
                throw new ClassStructure_Exception_InvalidMethodBinding($str_method, $clb_callable);
            }
            
        }
    }
    
    protected function __weaveAdviceBefore($clb_callable, $mix_pointcut, $arr_options = array()) {
        $this->__weaveAdvice($clb_callable, $mix_pointcut, self::WEAVE_ADVICE_BEFORE_BASE, $arr_options);
    }
    
    protected function __weaveAdviceAfter($clb_callable, $mix_pointcut, $arr_options = array()) {
        $this->__weaveAdvice($clb_callable, $mix_pointcut, self::WEAVE_ADVICE_AFTER_BASE, $arr_options);
    }
    
    protected function __weaveAdviceOnException($clb_callable, $mix_pointcut, $arr_options = array()) {
        $this->__weaveAdvice($clb_callable, $mix_pointcut, self::WEAVE_ADVICE_ON_EXCEPTION, $arr_options);
    }
    
    protected function __unweave() {
        
        // setup the before and after layers structure
        $this->arr_advices = array(
            self::WEAVE_ADVICE_BEFORE_BASE => array(),
            self::WEAVE_ADVICE_AFTER_BASE => array(),
            self::WEAVE_ADVICE_ON_EXCEPTION => array()
        );
        
    }
    
    /***************************************************************************
    / Utility and internal methods
    /**************************************************************************/
    
    /**
     * The internal method to add a layer to a particular method.
     *
     * @access private
     * @param string $str_method The method name to bind this layer.
     * @param string $enm_type Whether to put the method invocation before or
     *      after the base method. Defaults to 'after'.
     * @param callback $clb_callable The callable layer to bind.
     * @param array $arr_options An array of layer options.
     * @return void
     */
    private function addAdvice($str_method, $enm_type, $clb_callable, $arr_options = array()) {
        
        // initialise the layer, if not already present
        if(!array_key_exists($str_method, $this->arr_advices[$enm_type])) {
            $this->arr_advices[$enm_type][$str_method] = array();
        }
        
        // bind the layer to the method
        $arr_advice = array(
            'callable' => $clb_callable
        );
        
        if(isset($arr_options[self::OPTION_BREAK_CHAIN_ON_RETURN])) {
            $arr_advice[self::OPTION_BREAK_CHAIN_ON_RETURN] = $arr_options[self::OPTION_BREAK_CHAIN_ON_RETURN];
        }
        
        if($enm_type == self::WEAVE_ADVICE_BEFORE_BASE) {
            array_unshift($this->arr_advices[$enm_type][$str_method], $arr_advice);
        } else {
            array_push($this->arr_advices[$enm_type][$str_method], $arr_advice);
        }
        
    }
    
    protected function __hasAdvice($str_method, $enm_type) {
        
        if(
            isset($this->arr_advices[$enm_type]) && 
            isset($this->arr_advices[$enm_type][$str_method]) &&
            !empty($this->arr_advices[$enm_type][$str_method])
        ) {
            return true;
        }
        
        return false;
    }
    
    protected function __countAdvices($str_method, $enm_type) {
        
        if(
            isset($this->arr_advices[$enm_type]) && 
            isset($this->arr_advices[$enm_type][$str_method])
        ) {
            return count($this->arr_advices[$enm_type][$str_method]);
        }
        
        return 0;
    }
    
    
    /**
     * Ensures the named method exists in the base layer, or throws an exception.
     *
     * @access private
     * @param string $str_method The method to check for existence.
     * @return boolean
     */
    private function ensureMethodExists($str_method) {
        
        // does this method exist for the internal object?
        if(!$this->__hasMethod($str_method)) {
            $str_class = $this->__getClass($this->obj_internal);
            throw new ClassStructure_Exception_MethodDoesNotExist($str_method, $str_class);
        }

        return true;
        
    }
    
    /**
     * Gets the method signature of the internal object, as prepared during
     * object initialisation.
     *
     * @access protected
     * @param string $str_method The method name to retrieve.
     * @return string The method's signature.
     */
    protected function getMethod($str_method) {
        
        return $this->arr_methods[$str_method];
        
    }
    
    /**
     * Determines if the supplied method's signature matches the signature of
     * the Reflection function/method.
     *
     * @access protected
     * @param string $str_method The method to base the signature eligibility.
     * @param object $obj_method The Reflection function/method to check
     * @return boolean
     */
    protected function matchesMethodSignature($str_method, ReflectionFunctionAbstract $obj_method) {
        
        return $this->getMethod($str_method) === $this->getMethodSignature($obj_method);
        
    }
    
    /**
     * Gets the method signature of a Reflection function/method.
     *
     * This method merely prepends the method name to the front of the parameter
     * signature.
     *
     * Overwrite this method to have specific, more complex method signatures.
     *
     * @access protected
     * @param object $obj_method The Reflection function/method to extract the 
     *      signature.
     * @return string The method signature
     */
    protected function getMethodSignature(ReflectionFunctionAbstract $obj_method) {
        $str_method = $obj_method->name;
        return $str_method . '::' . $this->getParameterSignature($obj_method);
    }
    
    /**
     * Determines if the supplied method's parameter signature matches the 
     * parameter signature of the Reflection function/method.
     *
     * @access protected
     * @param string $str_method The method to base the signature eligibility.
     * @param object $obj_method The Reflection function/method to check
     * @return boolean
     */
    protected function matchesParameterSignature($str_method, ReflectionFunctionAbstract $obj_method) {
        
        // retrieve the method signature
        $str_method_signature = $this->getMethod($str_method);
        
        // extract just the parameter signature
        $str_parameter_signature = substr($str_method_signature, strpos($str_method_signature, '::')+2);
        
        // check equality
        return $str_parameter_signature === $this->getParameterSignature($obj_method);
        
    }
    
    /**
     * Gets the parameter signature of a Reflection function/method.
     *
     * This method gets each parameter's name from the Reflection class and then
     * creates a string from resulting parameter name collection.
     *
     * Overwrite this method to have specific, more complex method signatures.
     *
     * @access protected
     * @param object $obj_method The Reflection function/method to extract the 
     *      signature.
     * @return string The parameter signature
     */
    protected function getParameterSignature(ReflectionFunctionAbstract $obj_method) {
        
        $arr_parameters = array();
        foreach($obj_method->getParameters() as $obj_parameter) {
            $arr_parameters[] = $obj_parameter->getName();
        }
        
        // keep the signature readable, so just separate using a '|' character
        $str_signature = implode('|', $arr_parameters);
        
        return $str_signature;
        
    }
    
    /**
     * Determines whether the Reflection function/method has any parameters.
     *
     * @access protected
     * @param object $obj_method The Reflection function/method to check.
     * @return boolean
     */
    protected function hasParameters(ReflectionFunctionAbstract $obj_method) {
        
        return (count($obj_method->getParameters()) != 0);
        
    }
    
    protected function countParameters(ReflectionFunctionAbstract $obj_method) {
        
        return count($obj_method->getParameters());
        
    }
    
    /**
     * Parses an object for available methods and then generates a method
     * signature cache for later use.
     *
     * @access protected
     * @param object $obj The object to parse available method signatures.
     * @return array The array of available method signatures.
     */
    protected function parseAvailableMethods($obj) {
        
        $arr_methods = $this->getAvailableMethods($obj);
        
        foreach($arr_methods as $str_method => $obj_method) {
            $arr_methods[$str_method] = $this->getMethodSignature($obj_method);
        }
        
        return $arr_methods;
        
    }
    
    /**
     * Gets the available methods for an object using PHP Reflection.
     *
     * @access protected
     * @param object $obj The object to get the available methods.
     * @return array The array of available methods.
     */
    protected function getAvailableMethods($obj) {
        
        $arr_methods = array();
        
        // if the supplied object is an instance of this class, then get the
        // base layer of the supplied object
        if($obj instanceof self) {
            $obj = $obj->__getObject();
        }
        
        // get all the Reflection methods
        $obj_class = new ReflectionClass($obj);
        $arr_temp = $obj_class->getMethods();
            
        foreach($arr_temp as $str_key => $obj_method) {
            
            // only save public methods which are not the constructor or destructor
            if(
                $obj_method->isPublic() && 
                !$obj_method->isConstructor() &&
                !$obj_method->isDestructor()
            ) {
                $str_method = $obj_method->name;
                $arr_methods[$str_method] = $obj_method;
            }
        }
        
        return $arr_methods;
        
    }
    
    protected function formatMethodSignature($str_method, $arr_method) {
        
        $bln_advice_before     = $this->__hasAdvice($str_method, self::WEAVE_ADVICE_BEFORE_BASE);
        $bln_advice_after      = $this->__hasAdvice($str_method, self::WEAVE_ADVICE_AFTER_BASE);
        $bln_advice_exception  = $this->__hasAdvice($str_method, self::WEAVE_ADVICE_ON_EXCEPTION);
        
        if($bln_advice_before || $bln_advice_after || $bln_advice_exception) {
            $arr_method['visibility'] = 'weaved';
        }
        
        return parent::formatMethodSignature($str_method, $arr_method);
        
    }
    
    public function __getAdviceSignature($str_method) {
        
        $str_signature = null;
        
        $str_method_signature = null;
        $arr_methods = self::getMethodSignatures($this->obj_internal);
        foreach($arr_methods as $str_method_internal => $arr_method) {
            if($str_method_internal == $str_method) {
                $arr_method['visibility'] = 'virtual';
                $str_method_signature = self::formatMethodSignature($str_method, $arr_method);
                $str_method_signature = str_replace('  ', ' ', trim($str_method_signature));
                break;
            }
        }
        
        if(!empty($str_method_signature)) {
        
            $str_signature = $str_method_signature . "\n{\n";
            
            $arr_before    = $this->getAdviceSignaturesFor($str_method, self::WEAVE_ADVICE_BEFORE_BASE);
            $arr_after     = $this->getAdviceSignaturesFor($str_method, self::WEAVE_ADVICE_AFTER_BASE);
            $arr_exception = $this->getAdviceSignaturesFor($str_method, self::WEAVE_ADVICE_ON_EXCEPTION);
    
            foreach($arr_before as $str) {
                $str_signature .= '       before    ' . $str ."\n";
            }
            
            foreach($arr_after as $str) {
                $str_signature .= '       after     ' . $str ."\n";
            }
            
            foreach($arr_exception as $str) {
                $str_signature .= '       exception ' . $str ."\n";
            }
            
            $str_signature .= "}\n";
        }
        
        return $str_signature;
    }
    
    private function getAdviceSignaturesFor($str_method, $enm_advice) {
        
        $arr_signatures = array();
        
        // are there layers which should be called before the base layer?
        if(array_key_exists($str_method, $this->arr_advices[$enm_advice])) {
            
            $arr_advices = $this->arr_advices[$enm_advice][$str_method];
            
            // iterate through each layer and execute the corresponding callback
            foreach($arr_advices as $i => $arr_advice) {

                $clb_callable = $arr_advice['callable'];

                $str_callable = '';
                if(is_array($clb_callable)) {
                    $str_callable = get_class($clb_callable[0]) . '::' . $clb_callable[1];
                } else if(is_string($clb_callable)) {
                    $str_callable = $clb_callable;
                }
                
                if(!empty($str_callable)) {
                    
                    $arr_options = array();
                    if(
                        isset($arr_advice[self::OPTION_BREAK_CHAIN_ON_RETURN]) &&
                        ($arr_advice[self::OPTION_BREAK_CHAIN_ON_RETURN])
                    ) {
                        $arr_options[] = 'terminal';
                    }
                    
                    $str_callable .= '(...)';
                    
                    if(!empty($arr_options)) {
                        $str_callable .= ' <' . implode(', ', $arr_options) . '>';
                    }
                    
                    $arr_signatures[] = $str_callable;
                }
            }
            
        }
        
        return $arr_signatures;
    }

       
}

?>
